/**
 * Provides remote asynchronous service for communicating with the Twitter public server.
 */
package org.hackystat.ui.hackerrific.server;

import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import org.hackystat.ui.hackerrific.client.common.RPCException;
import org.hackystat.ui.hackerrific.client.common.RPCRemoteService;
import org.hackystat.ui.hackerrific.resource.statuses.jaxb.Statuses;
import org.hackystat.ui.hackerrific.resource.statuses.jaxb.Status;
import org.restlet.Client;
import org.restlet.data.ChallengeResponse;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Preference;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.resource.Representation;

/**
 * Provides remote asynchronous service for communicating with the Twitter public server.
 * @author David J. Nickles
 */
public class RPCServiceImpl extends RemoteServiceServlet implements RPCRemoteService {

  /** The serial version id. */
  private static final long serialVersionUID = -8176105485572090108L;
  /** The HTTP authentication approach. */
  private static ChallengeScheme scheme = ChallengeScheme.HTTP_BASIC;
  /** The preferred representation type. */
  private static Preference<MediaType> xmlMedia = new Preference<MediaType>(MediaType.TEXT_XML);
  /** Statuses (Twitter feed) JAXBContext. */
  private static final JAXBContext statusesJAXB;
  /** To facilitate debugging of problems using this system. */
  private boolean isTraceEnabled = false;
  
  // JAXBContexts are thread safe, so we can share them across all instances and threads.
  // https://jaxb.dev.java.net/guide/Performance_and_thread_safety.html
  static {
    try {
      statusesJAXB = JAXBContext
          .newInstance(org.hackystat.ui.hackerrific.resource.statuses.jaxb.ObjectFactory.class);
    }
    catch (Exception e) {
      throw new RuntimeException("Couldn't create JAXB context instances.", e);
    }
  }

  /**
   * Does the housekeeping for making HTTP requests to the Twitter server.
   * 
   * @param method The type of Method.
   * @param uri A string representation of the URI.
   * @param entity The representation to be sent with the request, or null if not needed.
   * @param user the authenticating user's name.
   * @param password the authenticating user's password.
   * @return The Response instance returned from the server.
   */
  private Response makeRequest(Method method, String uri, Representation entity, 
    String user, String password) {
    Client client = new Client(Protocol.HTTP);
    Reference reference = new Reference(uri);
    Request request = (entity == null) ? new Request(method, reference) : new Request(method,
        reference, entity);
    request.getClientInfo().getAcceptedMediaTypes().add(xmlMedia);
    ChallengeResponse authentication = new ChallengeResponse(scheme, user, password);
    request.setChallengeResponse(authentication);
    if (this.isTraceEnabled) {
      System.out.println("TwitterClient Tracing: " + method + " " + reference);
      if (entity != null) {
        try {
          System.out.println(entity.getText());
        }
        catch (Exception e) {
          System.out.println("  Problems with getText() on entity.");
        }
      }
    }
    Response response = client.handle(request);
    if (this.isTraceEnabled) {
      org.restlet.data.Status status = response.getStatus();
      System.out.println("  => " + status.getCode() + " " + status.getDescription());
    }
    return response;
  }

  /**
   * Returns an HTTP 200 OK response code and a format-specific response if authentication
   * was successful.  Use this method to test if supplied user credentials are valid with 
   * minimal overhead.
   * 
   * @param user the authenticating twitter user
   * @param password the authenticating twitter password
   * @return true if successful, false if not failed.
   */
  public Boolean authenticate(String user, String password) {
    Boolean result = Boolean.FALSE;
    String uri = "http://twitter.com/account/verify_credentials.xml";
    Response response = makeRequest(Method.GET, uri, null, user, password);
    if (response.getStatus().isSuccess()) {
      result = Boolean.TRUE;
    }
    log("Hackerrific: authenticate " + user + " status " + response.getStatus() + "\n");
    return result;
  }

  /**
   * Favorites the status specified in the ID parameter as the authenticating user.
   * Returns the favorite status when successful.
   * to add a favorite use: http://twitter.com/favourings/create/{id}
   * 
   * @param user the authenticating twitter user.
   * @param password the authenticating twitter password.
   * @param id Required.  The ID of the status to favorite.
   * @return true if successful, false if not failed.
   */
  public Boolean createFavorite(String user, String password, String id) {
    Boolean result = Boolean.FALSE;
    String uri = "http://twitter.com/favourings/create/" + id + ".xml";
    log("Hackerrific: create favorite " + uri + " for " + user + "\n");
    Response response = makeRequest(Method.POST, uri, null, user, password);
    if (response.getStatus().isSuccess()) {
      result = Boolean.TRUE;
    }
    log("Hackerrific: create favorite " + response.getStatus() + "\n");
    return result;
  }

  /**
   * Unfavorites the status specified in the ID parameter as the authenticating user.
   * Returns the unfavorited status when successful.
   * to destroy it use: http://twitter.com/favourings/destroy/{id} 
   * 
   * @param user the authenticating twitter user.
   * @param password the authenticating twitter password.
   * @param id Required.  The ID of the status to favorite.
   * @return true if successful, false if not failed.
   */
  public Boolean destroyFavorite(String user, String password, String id) {
    Boolean result = Boolean.FALSE;
    String uri = "http://twitter.com/favourings/destroy/" + id + ".xml";
    log("Hackerrific: destroy favorite " + uri + " for " + user + "\n");
    Response response = makeRequest(Method.POST, uri, null, user, password);
    if (response.getStatus().isSuccess()) {
      result = Boolean.TRUE;
    }
    log("Hackerrific: destroy favorite " + response.getStatus() + "\n");
    return result;
  }
  
  /**
   * Ends the session of the authenticating user, returning a null cookie.
   * 
   * @param user the authenticating twitter user
   * @param password the authenticating twitter password
   * @return true if successful, false if not failed.
   */
  public Boolean endSession(String user, String password) {
    Boolean result = Boolean.FALSE;
    String uri = "http://twitter.com/account/end_session";
    Response response = makeRequest(Method.GET, uri, null, user, password);
    if (response.getStatus().isSuccess()) {
      result = Boolean.TRUE;
    }
    log("Hackerrific: end_session for " + user + ", " + response.getStatus() + "\n");
    return result;
  }

  /**
   * Takes a String encoding of a Statuses feed in XML format and converts it to an instance.
   * 
   * @param xmlString The XML string representing a Statuses feed.
   * @return The corresponding Statuses instance.
   * @throws Exception If problems occur during unmarshalling.
   */
  private Statuses makeStatuses(String xmlString) throws Exception {
    Unmarshaller unmarshaller = statusesJAXB.createUnmarshaller();
    return (Statuses) unmarshaller.unmarshal(new StringReader(xmlString));
  }
    
  /**
   * Creates an array of strings for the sensor data instance in the SensorData instance.
   * 
   * index 0: XMLFormatDate of CreatedAt
   * index 1: String of Tweet Id
   * index 2: String of Text
   * index 3: String of Source
   * index 4: String of User
   * index 5: String of User Id
   * index 6: String of User Screen Name
   * index 7: String of User Location
   * index 8: String of User Description
   * index 9: String of User ProfileImageUrl
   * index 10: String of User Url
   * 
   * @param status A JAXB unmarshalled Status instance
   * @return String A formatted string of the elements of the tweets to present in the UI
   * @throws Exception thrown if problems occur during the formatting
   */
  private ArrayList<String> formatStatusForClient(Status status) throws Exception {
    ArrayList<String> list = new ArrayList<String>();
    list.add(SimpleDateFactories.getInstance().getGMTfactory().format(
        SimpleDateFactories.getInstance().getTwitterfactory().parse(status.getCreatedAt())));
    list.add(status.getId().toString());
    list.add(status.getText());
    list.add(status.getSource());
    list.add(status.getUser().getName());
    list.add(status.getUser().getId().toString());
    list.add(status.getUser().getScreenName());
    list.add(status.getUser().getLocation());
    list.add(status.getUser().getDescription());
    list.add(status.getUser().getProfileImageUrl());
    list.add(status.getUser().getUrl());
    return list;
  }

  /**
   * Returns the 20 most recent statuses from the Twitter server for the specified feed type.
   * @param user user to authentication.
   * @param password user password for authentication.
   * @param type the timeline type: public, friends, user, favorites, or direct_messages.
   * @param query (optional) get entries since the last entry id or last timestamp, or null.
   * @return Boolean array of days with data
   * @throws RPCException if problems occur during the SensorBase procedure call
   */
  public ArrayList<ArrayList<String>> timeline(String user, String password, String type, 
    String query) throws RPCException {
    String path;
    String attributes = query;
    if ("public".equals(type)) {
      path = "/statuses/" + type + "_timeline.xml";
      if (query != null) {
        attributes = "since_id=" + query;
      }
    }
    else if ("friends".equals(type) || "user".equals(type)) {
      path = "/statuses/" + type + "_timeline.xml";
      if (query != null) {
        try {
          String since = SimpleDateFactories.getInstance().getHttpfactory().format(
            SimpleDateFactories.getInstance().getGMTfactory().parse(query));
          attributes = "since=" + since;
        }
        catch (ParseException e) {
          log("Parse error: " + e.getMessage());
          attributes = null;
        }
      }
    } 
    else {
      path = "/" + type + ".xml";
    }
    log("Hackerrific: entering method get for " + user);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();  
    Statuses statuses = null;
    ArrayList<ArrayList<String>> list = new ArrayList<ArrayList<String>>();
    try {
      URI uri = new URI("http", "twitter.com", path, attributes, null);
      log("Hackerrific: get uri: " + uri.toURL().toString());
      Response response = makeRequest(Method.GET, uri.toURL().toString(), null, user, password);
      if (!response.getStatus().isSuccess()) {
        throw new RPCException(response.getStatus().toString());
      }
      try {
        String xmlData = response.getEntity().getText();
        statuses = makeStatuses(xmlData);
      }
      catch (Exception e) {
        throw new RPCException(response.getStatus().getDescription() + e);
      }
      for (Status p : statuses.getStatus()) {
        list.add(formatStatusForClient(p));
      }
      log("Hackerrific get: ArrayList populated with " + list.size() + " data values.\n");
    } 
    catch (Exception e) {
      log("Hackerrific get error: " + e.getMessage());
    }
    stopwatch.stop();
    log("Hackerrific get: exiting method with execution time of " + stopwatch + '\n');
    Collections.reverse(list);
    return list;
  }

  /**
   * Send a tweet to the Twitter server.
   * @param user user to authentication
   * @param password user password for authentication
   * @param message the tweet text
   * @return Boolean if the put was successful or not.
   * @throws RPCException if problems occur during the SensorBase procedure call
   */
  public Boolean update(String user, String password, String message) throws RPCException {
    Boolean result = Boolean.FALSE;
    log("Hackerrific: entering method update for " + user);
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.start();
    try {
      URI uri = new URI("http", "twitter.com", "/statuses/update.xml", "status=" + message, null);
      log("Hackerrific: update uri: " + uri.toURL().toString());
      Response response = makeRequest(Method.POST, uri.toURL().toString(), null, user, password);
      if (!response.getStatus().isSuccess()) {
        throw new RPCException(response.getStatus().toString());
      }
      result = Boolean.TRUE;
    } 
    catch (Exception e) {
      log("Hackerrific update error: " + e.getMessage());
    }
    stopwatch.stop();
    log("Hackerrific update: exiting method with execution time of " + stopwatch + '\n');
    return result;
  }
  
  /**
   * Returns the version number from the Manifest file.
   * 
   * @return String the version number from the Manifest file.
   */
  public String version() {
    String result = "Development";
    try {
      URL manifestURL = getServletContext().getResource("/META-INF/MANIFEST.MF");
      if (manifestURL == null) {
        log("Hackerrific version: Unable to get Manifest file.\n");
        return result;
      }
      Manifest mf = new Manifest(manifestURL.openStream());
      Attributes attr = mf.getMainAttributes();
      result = (String)attr.getValue("Implementation-Version");
      if (result == null) {
        log("Hackerrific version:"
          + "Unable to get 'Implementation-Version' attribute from the Manifest.\n");
        return result;
      }
    } 
    catch (Exception e) {
      log("Hackerrific version error: " + e.getMessage() + '\n');
    }
    log("Hackerrific version: Checking manifest, version=" + result + '\n');
    return result; 
  }
}
